package MusicLandscape.util;

import java.util.NoSuchElementException;
import java.util.Scanner;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 * Generic console function for getting a new value for an entity field from the user.
 *
 * <p>
 * Example:
 *
 * <pre>
 * Integer num = new ConsoleScanner(Integer::parseInt, (Integer i) -&gt; i &gt; 0, null)
 *     .scan("Give me a number")
 * </pre>
 *
 * Produces this output:
 *
 * <pre>
 * Give me a number: &lt;user inputs 123 and presses Enter&gt;
 * </pre>
 *
 * Leading to the variable <kbd>num</kbd> having the value <kbd>123</kbd>.
 *
 * @param <T> The type of the value being read
 * @author Jonas Altrock (ew20b126@technikum-wien.at)
 * @version 1
 * @since ExerciseSheet04
 */
public class ConsoleScanner<T> {
    /**
     * constant to allow readable indication of a skippable input
     */
    public static final boolean SKIPPABLE = true;
    /**
     * constant to allow readable indication of a non-skippable input
     */
    public static final boolean NOT_SKIPPABLE = false;

    /**
     * The default input scanner. Uses System.in if not set from outside.
     */
    public static Scanner defaultScanner;

    /**
     * Whether the input can be skipped (by pressing Enter for example).
     */
    public boolean skippable = SKIPPABLE;

    /**
     * The input to value transformer function.
     */
    public Function<String, T> transformer;

    /**
     * The input value validator function.
     */
    public Predicate<T> validator;

    /**
     * The scanner object in use.
     */
    public Scanner scanner;

    /**
     * A scanner for reading non-empty strings.
     */
    public static final ConsoleScanner<String> nonEmptyString = new ConsoleScanner<>((String s) -> s, (String s) -> !s.isBlank(), null);

    /**
     * A scanner for reading positive integers.
     */
    public static final ConsoleScanner<Integer> positiveInteger = new ConsoleScanner<>(Integer::parseInt, (Integer i) -> i >= 0, null);

    /**
     * Create a scanner for a single value. Skippable by default.
     *
     * @param transformer a function that transforms the input string to a value
     * @param validator a function that validates the given value
     * @param scanner optional Scanner to use, pass null to use the default scanner (System.in)
     */
    public ConsoleScanner(
            Function<String, T> transformer,
            Predicate<T> validator,
            Scanner scanner
    ) {
        this.transformer = transformer;
        this.validator = validator;

        if (scanner != null) {
            this.scanner = scanner;
        }
    }

    /**
     * Create a scanner for a single value.
     *
     * @param transformer a function that transforms the input string to a value
     * @param validator a function that validates the given value
     * @param scanner optional Scanner to use, pass null to use the default scanner (System.in)
     * @param skippable whether the user is allowed to skip entry by just pressing Enter
     */
    public ConsoleScanner(
            Function<String, T> transformer,
            Predicate<T> validator,
            Scanner scanner,
            boolean skippable
    ) {
        this(transformer, validator, scanner);
        this.skippable = skippable;
    }

    /**
     * Duplicate this object and use a different Scanner.
     *
     * @param s the Scanner to use
     * @return a new ConsoleScanner using the provided Scanner
     */
    public ConsoleScanner<T> withScanner(Scanner s) {
        return new ConsoleScanner<>(transformer, validator, s, skippable);
    }

    /**
     * Duplicate this object and make it unskippable.
     *
     * @return a new ConsoleScanner that is not skippable.
     */
    public ConsoleScanner<T> unskippable() {
        return new ConsoleScanner<>(transformer, validator, scanner, NOT_SKIPPABLE);
    }

    /**
     * Get the default scanner object. System.in by if not set otherwise.
     *
     * @return the default scanner
     */
    public Scanner getDefaultScanner() {
        if (defaultScanner != null) {
            return defaultScanner;
        }
        return new Scanner(System.in);
    }

    /**
     * Get Scanner in use
     * @return Scanner
     */
    public Scanner getScanner() {
        if (scanner != null) {
            return scanner;
        }

        return getDefaultScanner();
    }

    /**
     * Prompt the user for an input value.
     *
     * @param prompt string to print before prompt input
     * @return a value or null, if no value was given
     */
    public T scan(String prompt) {
        T value;

        do {
            System.out.print(prompt + ": ");

            try {
                String in = getScanner().nextLine();

                if (skippable && in.isEmpty()) {
                    return null;
                }

                value = transformer.apply(in);
            } catch (NoSuchElementException e) {
                value = null;

                if (skippable) {
                    return null;
                }
            } catch (Exception e) {
                value = null;
            }

            if (value == null) {
                System.out.println("Invalid input, try again?");
                continue;
            }

            if (!validator.test(value)) {
                System.out.println("Value not allowed, try again?");
                value = null;
            }
        } while (value == null);

        return value;
    }
}
